/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.gui;

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.WindowConstants;
import javax.swing.text.BadLocationException;

import fr.x9c.cadmium.kernel.CadmiumException;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.Fatal;
import fr.x9c.cadmium.kernel.Interpreter;
import fr.x9c.cadmium.util.Misc;

/**
 * This class implements class encapsulating an interpreter and is a panel
 * with four elements:
 * <ul>
 *   <li>a top text area for interpreter output;</li>
 *   <li>a bottom text area for interpreter input;</li>
 *   <li>a button that open the 'history' window;</li>
 *   <li>a button that sends the entered input to the underlying interpreter
 *       (text can also be send by typing CTRL-enter in the bottom text area).</li>
 * </ul>
 * <br/>
 * The underlying interpreter is run in a thread that is launched at the end
 * of the constructor. <br/>
 * <br/> <br/>
 * Instances of this class should not be serialized, as they are stream-based.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
public final class InOutPanel extends JPanel implements Runnable {

    /** Serialization UID. */
    static final long serialVersionUID = 3237743495969414638L;

    /** Font name for output text area. */
    private static final String OUTPUT_FONT = "Monospaced";

    /** Text for 'send' button. */
    private static final String SEND = "send";

    /** Text for 'history' button. */
    private static final String HISTORY = "history";

    /** Text for 'save' button. */
    private static final String SAVE = "save";

    /** Text for 'close' button. */
    private static final String CLOSE = "close";

    /** Error title (when history cannot be written to file). */
    private static final String ERROR_TITLE = "Error";

    /** Error message (when history cannot be written to file). */
    private static final String ERROR_MESSAGE = "Unable to write to file";

    /** Size of dialog borders. */
    private static final int BORDER = 16;

    /** Underlying interpreter. */
    private final transient Interpreter interpreter;

    /** Text are for standard output/error. */
    private final JTextArea output;

    /** Scrool pane for standard output/error. */
    private final JScrollPane scroll;

    /** Text are for standard input. */
    private final JTextArea source;

    /** History list (last command at list head). */
    private final List<String> history;

    /** History position. */
    private int position;

    /** Standard input. */
    private final transient PrintStream standardInput;

    /** Standard output. */
    private final transient InputStream standardOutput;

    /** Standard error. */
    private final transient InputStream standardError;

    /**
     * Action associated with 'send' button.
     */
    private final Action sendAction = new AbstractAction() {

            /** Serialization UID. */
            static final long serialVersionUID = 3237743495969414638L;

            /**
             * Sends the text in the input text area (with a trailing new line)
             * to the underlying interpreter and set the text in the input area
             * to the empty string.
             * @param ae ignored
             */
            public void actionPerformed(final ActionEvent ae) {
                final String text = InOutPanel.this.source.getText();
                InOutPanel.this.standardInput.println(text);
                synchronized (InOutPanel.this.output) {
                    InOutPanel.this.output.append(text + "\n");
                } // end synchronized
                InOutPanel.this.source.setText("");
                InOutPanel.this.source.grabFocus();
                InOutPanel.this.history.add(0, text);
                InOutPanel.this.position = -1;
                InOutPanel.this.repaint();
            } // end method 'actionPerformed(ActionEvent)'

        };

    /**
     * Constructs a panel from interpreter an standatd input/output/error. <br/>
     * Starts a thread for interpreter execution.
     * @param interp interpreter
     * @param in standard input - should not be <tt>null</tt>
     * @param out standard output - should not be <tt>null</tt>
     * @param err standard error - should not be <tt>null</tt>
     */
    public InOutPanel(final Interpreter interp,
                      final PrintStream in,
                      final InputStream out,
                      final InputStream err) {
        super(new BorderLayout());
        assert interp != null : "null interp";
        assert in != null : "null in";
        assert out != null : "null out";
        assert err != null : "null err";
        this.interpreter = interp;
        this.standardInput = in;
        this.standardOutput = out;
        this.standardError = err;
        this.history = new LinkedList<String>();
        this.position = -1;

        // text areas
        final JPanel areasPanel = new JPanel(new GridLayout(2, 1, 4, 4));
        this.output = new JTextArea();
        this.output.setName("fr.x9c.cadmium.gui.InOutPanel.output");
        this.output.setFont(new Font(InOutPanel.OUTPUT_FONT,
                                     Font.PLAIN,
                                     this.output.getFont().getSize()));
        this.output.setEditable(false);
        this.source = new JTextArea();
        this.source.setName("fr.x9c.cadmium.gui.InOutPanel.source");
        final Object enterKey = new Object();
        this.source.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,
                                                             InputEvent.CTRL_MASK,
                                                             true),
                                      enterKey);
        this.source.getActionMap().put(enterKey, this.sendAction);
        final Object upKey = new Object();
        this.source.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,
                                                             InputEvent.CTRL_MASK,
                                                             true),
                                      upKey);
        this.source.getActionMap().put(upKey, new HistoryNavigator(1));
        final Object downKey = new Object();
        this.source.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,
                                                             InputEvent.CTRL_MASK,
                                                             true),
                                      downKey);
        this.source.getActionMap().put(downKey, new HistoryNavigator(-1));
        this.scroll =
            new JScrollPane(this.output,
                            ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                            ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        areasPanel.add(this.scroll);
        areasPanel.add(new JScrollPane(this.source,
                                       ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                                       ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));

        // history & send button
        final JPanel buttonsPanel = new JPanel(new BorderLayout());
        final JButton historyButton = new JButton(InOutPanel.HISTORY);
        historyButton.setName("fr.x9c.cadmium.gui.InOutPanel.history");
        historyButton.addActionListener(new ActionListener() {
                public void actionPerformed(final ActionEvent ae) {
                    new HistoryDialog();
                } // end method 'actionPerformed(ActionEvent)'
            });
        final JButton sendButton = new JButton(InOutPanel.SEND);
        sendButton.setName("fr.x9c.cadmium.gui.InOutPanel.send");
        sendButton.addActionListener(this.sendAction);
        buttonsPanel.add(historyButton, BorderLayout.WEST);
        buttonsPanel.add(Box.createHorizontalGlue(), BorderLayout.CENTER);
        buttonsPanel.add(sendButton, BorderLayout.EAST);

        // main panel
        final JPanel mainPanel = new JPanel(new BorderLayout());
        mainPanel.add(areasPanel, BorderLayout.CENTER);
        mainPanel.add(buttonsPanel, BorderLayout.SOUTH);

        // content pane
        add(Box.createVerticalStrut(InOutPanel.BORDER), BorderLayout.NORTH);
        add(Box.createVerticalStrut(InOutPanel.BORDER), BorderLayout.SOUTH);
        add(Box.createHorizontalStrut(InOutPanel.BORDER), BorderLayout.EAST);
        add(Box.createHorizontalStrut(InOutPanel.BORDER), BorderLayout.WEST);
        add(mainPanel, BorderLayout.CENTER);
        this.source.requestFocus();
        (new Copy(out)).start();
        (new Copy(err)).start();
        (new Thread(this)).start();
    } // end constructor(Interpreter, PrintStream, InputStream, InputStream)

    /**
     * Executes the underlying interpreter.
     */
    public void run() {
        try {
            this.interpreter.execute();
        } catch (final CadmiumException ie) {
            final Throwable t = ie.getCause();
            if (!(t instanceof Fail.Exception)
                && !(t instanceof Fatal.Exception)) {
                synchronized (this.output) {
                    this.output.append("error: " + ie.getMessage());
                } // end synchronized
            } // end if
        } catch (final Exception e) {
            synchronized (this.output) {
                this.output.append("internal error: " + e.getMessage());
            } // end synchronized
        } catch (final Throwable t) {
            synchronized (this.output) {
                this.output.append("Java error: " + t.getMessage());
            } // end synchronized
        } // end try/catch
    } // end method 'run()'

    /**
     * This class implements a thread copying data from an input stream
     * into a text area.
     */
    private final class Copy extends Thread {

        /** Size of internal buffer, in bytes. */
        private static final int BUFFER_SIZE = 1024;

        /** Data source. */
        private final InputStream source;

        /**
         * Constructs a thread copying data from an input stream
         * into a text area.
         * @param src data source - should not be <tt>null</tt>
         */
        private Copy(final InputStream src) {
            assert src != null : "null src";
            this.source = src;
        } // end constructor(InputStream)

        /**
         * Copies data from source to destination.
         */
        public void run() {
            try {
                final byte[] buffer = new byte[InOutPanel.Copy.BUFFER_SIZE];
                int nb = this.source.read(buffer);
                while (nb != -1) {
                    synchronized (InOutPanel.this.output) {
                        InOutPanel.this.output.append(Misc.convertBytesToString(buffer, 0, nb));
                        try {
                            final int len =
                                InOutPanel.this.output.getDocument().getLength();
                            final int line =
                                InOutPanel.this.output.getLineOfOffset(len);
                            final int pos =
                                InOutPanel.this.output.getLineStartOffset(line);
                            InOutPanel.this.output.setCaretPosition(pos);
                        } catch (final BadLocationException ble) {
                            // nothing to do
                        } // end try/catch
                    } // end synchronized
                    InOutPanel.this.scroll.repaint();
                    nb = this.source.read(buffer);
                } // end while
            } catch (final IOException ioe) {
                // should not happen
            } // end try/catch
        } // end method 'run()'

    } // end inner-class 'Copy'

    /**
     * This class implements a modal dialog showing the history and allowing
     * to save it to a file.
     */
    private final class HistoryDialog extends JDialog {

        /** Serialization UID. */
        static final long serialVersionUID = -1057305832727479511L;

        /**
         * Constructs and shows the dialog.
         */
        private HistoryDialog() {
            super((Frame) null, "History", true);
            setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
            final JList list = new JList(InOutPanel.this.history.toArray());
            list.addMouseListener(new MouseAdapter() {
                    public void mouseClicked(final MouseEvent me) {
                        if (me.getClickCount() == 2) {
                            final int index = list.locationToIndex(me.getPoint());
                            final Object item = list.getModel().getElementAt(index);
                            InOutPanel.this.source.setText(item.toString());
                        } // end if
                    } // end method 'mouseClicked(MouseEvent)'
                });

            // save & close button
            final JPanel buttonsPanel = new JPanel(new BorderLayout());
            final JButton saveButton = new JButton(InOutPanel.SAVE);
            saveButton.addActionListener(new ActionListener() {
                    public void actionPerformed(final ActionEvent ae) {
                        final JFileChooser fc = new JFileChooser();
                        if (fc.showSaveDialog(InOutPanel.HistoryDialog.this) == JFileChooser.APPROVE_OPTION) {
                            try {
                                final FileWriter out = new FileWriter(fc.getSelectedFile());
                                final ListIterator<String> it =
                                    InOutPanel.this.history.listIterator(InOutPanel.this.history.size());
                                while (it.hasPrevious()) {
                                    out.write(it.previous());
                                    out.write("\n\n");
                                } // end while
                                out.close();
                            } catch (final IOException ioe) {
                                JOptionPane.showMessageDialog(InOutPanel.HistoryDialog.this,
                                                              InOutPanel.ERROR_MESSAGE,
                                                              InOutPanel.ERROR_TITLE,
                                                              JOptionPane.ERROR_MESSAGE);
                            } // end try/catch
                        } // end if
                    } // end method 'actionPerformed(ActionEvent)'
                });
            final JButton closeButton = new JButton(InOutPanel.CLOSE);
            closeButton.addActionListener(new ActionListener() {
                    public void actionPerformed(final ActionEvent ae) {
                        InOutPanel.HistoryDialog.this.setVisible(false);
                    } // end method 'actionPerformed(ActionEvent)'
                });
            buttonsPanel.add(saveButton, BorderLayout.WEST);
            buttonsPanel.add(Box.createHorizontalGlue(), BorderLayout.CENTER);
            buttonsPanel.add(closeButton, BorderLayout.EAST);

            // main panel
            final JPanel mainPanel = new JPanel(new BorderLayout());
            mainPanel.add(new JScrollPane(list,
                                          ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                                          ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER);
            mainPanel.add(buttonsPanel, BorderLayout.SOUTH);

            // content pane
            add(Box.createVerticalStrut(InOutPanel.BORDER), BorderLayout.NORTH);
            add(Box.createVerticalStrut(InOutPanel.BORDER), BorderLayout.SOUTH);
            add(Box.createHorizontalStrut(InOutPanel.BORDER), BorderLayout.EAST);
            add(Box.createHorizontalStrut(InOutPanel.BORDER), BorderLayout.WEST);
            add(mainPanel, BorderLayout.CENTER);
            setSize(400, 300);
            setResizable(false);
            setVisible(true);
        } // end empty constructor

    } // end inner-class 'HistoryDialog'

    /**
     * This class implements an action that naviagtes in the history,
     * setting the source to the history element.
     */
    private final class HistoryNavigator extends AbstractAction {

        /** Serialization UID. */
        static final long serialVersionUID = 123654288864806406L;

        /** Delta used for history navigation. */
        private final int delta;

        /**
         * Constructs an history navigator from a delta.
         * @param delta delta used for history navigation
         */
        private HistoryNavigator(final int delta) {
            this.delta = delta;
        } // end constructor(int)

        /**
         * Navigates in the history.
         * @param ae ignored
         */
        public void actionPerformed(final ActionEvent ae) {
            InOutPanel.this.position += this.delta;
            final int len = InOutPanel.this.history.size();
            if (InOutPanel.this.position >= len) {
                InOutPanel.this.position = len - 1;
            } // end if
            if (InOutPanel.this.position < 0) {
                InOutPanel.this.position = 0;
            } // end if
            if ((InOutPanel.this.position >= 0) && (InOutPanel.this.position < len)) {
                final String s = InOutPanel.this.history.get(InOutPanel.this.position);
                InOutPanel.this.source.setText(s);
                InOutPanel.this.repaint();
            } // end if
        } // end method 'actionPerformed(ActionEvent)'

    } // end inner-class 'HistoryNavigator'

} // end class 'InOutPanel'
